<!DOCTYPE html>

<head>
  <style>
    * {
      font-family: sans-serif;
    }
    pre {
      font-family: monospace;
    }
    a {
      font-family: sans-serif;
    }
    audio {
      width: 100%;
    }
    canvas {
      width: 100%;
      height: 0;
      transition: all linear 0.1s;
    }
    .canvas-active {
      height: 15vh;
    }
    pre {
      overflow: scroll;
    }
  </style>
</head>

<body>
  <h1>
    听歌识曲 Demo (Credit:
    <a href="https://github.com/mos9527/ncm-afp" target="_blank"
      >https://github.com/mos9527/ncm-afp</a
    >)
  </h1>
  <p>Usage:</p>
  <li>Select your audio file through "Choose File" picker</li>
  <li>Seek to a point where your music should sound the most distinct</li>
  <li>Hit the "Clip" button and wait for the results!</li>
  <p>
    Sorry if your music somehow sounds 100x awful here, since everything is in
    <i>telephone quality</i> and that's what <i>they</i>'re using :/
  </p>

  <audio id="audio" controls autoplay></audio>
  <canvas id="canvas"></canvas>
  <button id="invoke">Clip</button>
  <input type="file" name="picker" accept="*" id="file" />
  <hr />
  <label for="use-mic">Listen from microphone</label>
  <input type="checkbox" name="use-mic" id="usemic" />
  <hr />
  <pre id="logs"></pre>
</body>
<script type="module">
  import { InstantiateRuntime, GenerateFP } from './afp.js'
  const duration = 5

  let audioCtx, recorderNode, micSourceNode
  let audioBuffer, bufferHealth
  let runtime = InstantiateRuntime()
  let audio = document.getElementById('audio')
  let file = document.getElementById('file')
  let clip = document.getElementById('invoke')
  let usemic = document.getElementById('usemic')
  let canvas = document.getElementById('canvas')
  let canvasCtx = canvas.getContext('2d')
  let logs = document.getElementById('logs')
  logs.write = (line) => (logs.innerHTML += line + '\n')

  function RecorderCallback(channelL) {
    let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
    let FP = GenerateFP(sampleBuffer)
    logs.write(`[index] Generated FP ${FP}`)
    logs.write('[index] Now querying, please wait...')
    fetch(
      '/audio/match?' +
        new URLSearchParams(
          Object.assign({
            audioFP: FP,
            duration: duration,
          }),
        ),
    )
      .then((resp) => resp.json())
      .then((resp) => {
        if (!resp.data.result) {
          return logs.write('[index] Query failed with no results.')
        }
        logs.write(`[index] Query complete. Results=${resp.data.result.length}`)
        for (var song of resp.data.result) {
          logs.write(
            `<a target="_blank" href="https://music.163.com/song?id=${
              song.song.id
            }">${song.song.name} - ${song.song.album.name} (${
              song.startTime / 1000
            }s)</a>`,
          )
        }
      })
  }

  function InitAudioCtx() {
    // AFP.wasm can't do it with anything other than 8KHz
    audioCtx = new AudioContext({ sampleRate: 8000 })
    if (audioCtx.state == 'suspended') return false
    let audioNode = audioCtx.createMediaElementSource(audio)
    audioCtx.audioWorklet.addModule('rec.js').then(() => {
      recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
      audioNode.connect(recorderNode) // recorderNode doesn't output anything
      audioNode.connect(audioCtx.destination)
      recorderNode.port.onmessage = (event) => {
        switch (event.data.message) {
          case 'finished':
            RecorderCallback(event.data.recording)
            clip.innerHTML = 'Clip'
            clip.disabled = false
            canvas.classList.remove('canvas-active')
            break
          case 'bufferhealth':
            clip.innerHTML = `${(duration * (1 - event.data.health)).toFixed(
              2,
            )}s`
            bufferHealth = event.data.health
            audioBuffer = event.data.recording
            break
          default:
            logs.write(event.data.message)
        }
      }
      // Attempt to get user's microphone and connect it to the AudioContext.
      navigator.mediaDevices
        .getUserMedia({
          audio: {
            echoCancellation: false,
            autoGainControl: false,
            noiseSuppression: false,
            latency: 0,
          },
        })
        .then((micStream) => {
          micSourceNode = audioCtx.createMediaStreamSource(micStream)
          micSourceNode.connect(recorderNode)
          usemic.checked = true
          logs.write('[rec.js] Microphone attached.')
        })
    })
    return true
  }

  runtime.then(() => logs.write('[index] Wasm module loaded.'))
  clip.addEventListener('click', (event) => {
    recorderNode.port.postMessage({
      message: 'start',
      duration: duration,
    })
    clip.disabled = true
    canvas.classList.add('canvas-active')
  })
  usemic.addEventListener('change', (event) => {
    if (!usemic.checked) micSourceNode.disconnect(recorderNode)
    else micSourceNode.connect(recorderNode)
  })
  file.addEventListener('change', (event) => {
    file.files[0].arrayBuffer().then(async (buffer) => {
      await runtime
      logs.write(`[index] File ${file.files[0].name} loaded.`)
      audio.src = window.URL.createObjectURL(new Blob([buffer]))
      clip.disabled = false
    })
  })

  function UpdateCanvas() {
    let w = canvas.clientWidth,
      h = canvas.clientHeight
    ;(canvas.width = w), (canvas.height = h)
    canvasCtx.fillStyle = 'rgba(0,0,0,0)'
    canvasCtx.fillRect(0, 0, w, h)
    if (audioBuffer) {
      canvasCtx.fillStyle = 'black'
      for (var x = 0; x < w * bufferHealth; x++) {
        var y = audioBuffer[Math.ceil((x / w) * audioBuffer.length)]
        var z = (Math.abs(y) * h) / 2
        canvasCtx.fillRect(x, h / 2 - (y > 0 ? z : 0), 1, z)
      }
    }
    requestAnimationFrame(UpdateCanvas)
  }
  UpdateCanvas()
  let requestCtx = setInterval(() => {
    try {
      if (InitAudioCtx()) {
        // Put this here so we don't have to deal with the 'user did not interact' thing
        clearInterval(requestCtx)
        logs.write('[rec.js] Audio Context started.')
      }
    } catch {
      // Fail silently
    }
  }, 100)
</script>
